package pres.moling.auth.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import pres.moling.auth.service.AuthRememberMeServices;
import pres.moling.auth.service.AuthLoginFilter;
import pres.moling.auth.service.AuthUserDetailsServiceImpl;
import pres.moling.auth.service.AuthenticationHandler;

import javax.sql.DataSource;
import java.util.List;

/**
 * 核心配置
 */
@Configuration
@RequiredArgsConstructor
public class AuthSecurityConfig {
    /**
     * @Bean authenticationManager 提供了MyLoginFilter需要的AuthenticationManager
     * @Bean daoAuthenticationProvider提供了MyRememberMeServices需要的PersistentTokenRepository，其中setCreateTableOnStartup方法在首次运行的时候需要解开注释让它自动建表
     * @Bean securityFilterChain核心中的核心，2.x版本中对HttpSecurity http的配置都需要移动到这里，这里我们配置了：
     * 路径配置，这里把接口文档和验证码的路径进行了放行，其他请求都需要认证。登陆请求并不受它影响不需要专门配置。
     * 用自定义的MyLoginFilter替换了默认的UsernamePasswordAuthenticationFilter，注意原本的http.formLogin()不要再写了，否则将可以通过/login绕过验证码登陆
     * 登出配置，指定了路径，和成功登出的处理方法
     * csrf验证，注意这里比2.x版本需要多写一句.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
     * 会话管理，配置了只允许一个端登陆，不需要配置sessionRegistry了，会自动注入，当然手动配置也是可以的，但是容器里不会自动创建了，需要手动传一个new SpringSessionBackedSessionRegistry<>(new RedisIndexedSessionRepository(redisTemplate))，其中redisTemplate需要为RedisTemplate<String,Object>
     * 记住我功能，注意，这里和MyLoginFilter里的两次配置缺一不可。
     * 权限不足时的处理
     */

    /**
     * 接口文档放行
     */
    public static final List<String> DOC_WHITE_LIST = List.of("/doc.html", "/webjars/**", "/v3/api-docs/**");
    /**
     * 验证码放行
     */
    public static final List<String> VERIFY_CODE_WHITE_LIST = List.of("/sys/verifyCode/**");

    /**
     * 获取AuthenticationManager（认证管理器），登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 允许抛出用户不存在的异常
     *
     * @param myUserDetailsService myUserDetailsService
     * @return DaoAuthenticationProvider
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(AuthUserDetailsServiceImpl myUserDetailsService) {
        final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(myUserDetailsService);
        provider.setUserDetailsPasswordService(myUserDetailsService);
        provider.setHideUserNotFoundExceptions(false);
        return provider;
    }

    /**
     * 自定义RememberMe服务token持久化仓库
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource datasource) {
        final JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
//        tokenRepository.set(datasource);
        //第一次启动的时候建表
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                   AuthLoginFilter loginFilter,
                                                   AuthenticationHandler authenticationHandler,
                                                   AuthRememberMeServices rememberMeServices
    ) throws Exception {


        //路径配置
        return http.authorizeHttpRequests((requestMatchers) -> {
                    requestMatchers.requestMatchers(HttpMethod.GET, DOC_WHITE_LIST.toArray(new String[0])).permitAll()
                            .requestMatchers(HttpMethod.GET, VERIFY_CODE_WHITE_LIST.toArray(new String[0])).permitAll()
//                .requestMatchers(HttpMethod.GET, TEST_WHITE_LIST.toArray(new String[0])).permitAll()
                            .anyRequest().authenticated();
                })
                //登陆
                .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
                //配置自定义登陆流程后需要关闭 否则可以使用原有登陆方式

                //登出
                .logout((r) -> r.logoutUrl("/sys/user/logout").logoutSuccessHandler(authenticationHandler))
                // 禁用 csrf
//                .csrf(AbstractHttpConfigurer::disable)
                .csrf((csrf) -> {
                    //csrf验证 存储到Cookie中
                    csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler());
                }).sessionManagement((session) -> {
                    //  会话管理
                    session.maximumSessions(1)
                            .expiredSessionStrategy(authenticationHandler);
                    //引入redis-session依赖后已不再需要手动配置 sessionRegistry
//                .sessionRegistry(new SpringSessionBackedSessionRegistry<>(new RedisIndexedSessionRepository(RedisConfig.createRedisTemplate())))
                    //禁止后登陆挤下线
//               .maxSessionsPreventsLogin(true)
                    ;
                })
                //rememberMe
                .rememberMe((remember) -> remember.rememberMeServices(rememberMeServices))
                .exceptionHandling((exceptionHandlingCustomizer) -> {
                    // 权限不足时的处理
                    exceptionHandlingCustomizer.accessDeniedHandler(authenticationHandler)
                            .authenticationEntryPoint(authenticationHandler);
                }).build();


    }
}
